﻿//==============================================================================
// File: Job.cs
// Created: 2010-06-07
// Author: Piotr Włodek, Arkadiusz Świerczek
//==============================================================================
// This file is a part of MAMMOTH project.
// Copyright (C) 2010 AGH University of Science and Technology, Krakow.
// https://caribou.iisg.agh.edu.pl/trac/mammoth
//
// Mammoth is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// 
// Mammoth is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
// 
// You should have received a copy of the GNU Lesser General Public License
// along with Mammoth. If not, see http://www.gnu.org/licenses/.
//==============================================================================

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.Remoting.Messaging;
using System.Threading;
using WpfRcp.Core.Operations;
using WpfRcp.Core.Properties;
using WpfRcp.Core.Services;
using WpfRcp.Core.Threading.Internal;
using Microsoft.Practices.ServiceLocation;

namespace WpfRcp.Core.Threading
{
    /// <summary>
    /// Delegate that the job is able to execute.
    /// </summary>
    public delegate void JobStart(object state);

    /// <summary>
    /// Represents a job.
    /// </summary>
    public sealed class Job : INotifyPropertyChanged, IDisposable
    {
        #region Public Constructors

        /// <summary>
        /// Constructs a new Job object.
        /// </summary>
        /// <param name="start"><see cref="JobStart"/> delegate.</param>
        /// <param name="name">The name of the job.</param>
        public Job(JobStart start, string name)
            : this(start, name, JobOptions.Default)
        {
        }

        /// <summary>
        /// Constructs a new job object.
        /// </summary>
        /// <param name="start"></param>
        /// <param name="name">The name of the job.</param>
        /// <param name="options"></param>
        public Job(JobStart start, string name, JobOptions options)
        {
            m_ExecutionDelegate = start;
            Name = name;
            Options = options;
        }

        /// <summary>
        /// Constructs a new job object.
        /// </summary>
        /// <param name="operation"><see cref="IOperation"/> operation to be executed by this job.</param>
        public Job(IOperation operation)
            : this(operation, new JobOptions(operation.IsCancelable, operation.IsBackgroundable))
        {
        }

        /// <summary>
        /// Constructs a new job object.
        /// </summary>
        /// <param name="operation"><see cref="IOperation"/> operation to be executed by this job.</param>
        /// <param name="options"></param>
        public Job(IOperation operation, JobOptions options)
        {
            m_ExecutionDelegate = x => operation.Execute();

            Name = operation.Text;
            Options = options;

            AddLogging(operation);
        }

        #endregion

        #region Public Events

        private PropertyChangedEventHandler m_PropertyChanged;

        /// <summary>
        /// Raised whenever property of the operation changes.
        /// </summary>
        event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
        {
            add { m_PropertyChanged += value; }
            remove { m_PropertyChanged -= value; }
        }

        /// <summary>
        /// Event indicating that the operation has completed.
        /// </summary>
        public event EventHandler<EventArgs> Completed;

        /// <summary>
        /// Event indicating that the operation has completed.
        /// </summary>
        public event EventHandler<EventArgs> Cancelled;

        /// <summary>
        /// Raised whenever an error occures in the operation.
        /// </summary>
        public event EventHandler<JobErrorEventArgs> Error;

        #endregion
        
        /// <summary>
        /// Contains a list of all operations that have been executed.
        /// </summary>
        public static readonly ObservableCollection<Job> AllJobs = new ObservableCollection<Job>();

        private readonly JobStart m_ExecutionDelegate;

        /// <summary>
        /// This var stores info about calling thread's culture when executiong in asynchronous mode.
        /// Using this info, worker thread will have same culture info.
        /// </summary>
        private CultureInfo m_CultureInfo;

        private JobOptions m_Options;
        private DateTime m_StartTime;
        private DateTime m_EndTime;
        private JobState m_State;
        private object m_AsyncState;
        private object m_AsyncParameter;
        private string m_Name;

        /// <summary>
        /// Gets the async parameter that was passed to the <see cref="RunAsync()"/> method.
        /// </summary>
        public object AsyncParameter
        {
            get { return m_AsyncParameter; }
            private set
            {
                m_AsyncParameter = value;
                OnPropertyChanged("AsyncParameter");
            }
        }

        /// <summary>
        /// Gets the name of the job.
        /// </summary>
        public string Name
        {
            get { return m_Name; }
            private set
            {
                m_Name = value;
                OnPropertyChanged("Name");
            }
        }

        /// <summary>
        /// Gets an asynchronous execution state.
        /// </summary>
        public object AsyncState
        {
            get { return m_AsyncState; }
            private set
            {
                m_AsyncState = value;
                OnPropertyChanged("AsyncState");
            }
        }

        /// <summary>
        /// Returns the start time of the Job.
        /// </summary>
        public DateTime StartTime
        {
            get { return m_StartTime; }
            private set
            {
                if (value != m_StartTime)
                {
                    m_StartTime = value;
                    EndTime = m_StartTime;
                    OnPropertyChanged("StartTime");
                }
            }
        }

        /// <summary>
        /// Returns the stop time of the job.
        /// </summary>
        public DateTime EndTime
        {
            get { return m_EndTime; }
            private set
            {
                if (value != m_EndTime)
                {
                    m_EndTime = value;
                    OnPropertyChanged("EndTime");
                    OnPropertyChanged("TimeElapsed");
                }
            }
        }

        /// <summary>
        /// Returns total time the operation took to complete.
        /// </summary>
        public TimeSpan TimeElapsed
        {
            get { return m_EndTime - m_StartTime; }
        }

        /// <summary>
        /// Gets the state of the job.
        /// </summary>
        public JobState State
        {
            get { return m_State; }
            private set
            {
                m_State = value;
                OnPropertyChanged("State");
            }
        }

        /// <summary>
        /// Gets the job options.
        /// </summary>
        public JobOptions Options
        {
            get { return m_Options; }
            private set
            {
                m_Options = value;
                OnPropertyChanged("Options");
            }
        }

        /// <summary>
        /// Cancels the operation.
        /// </summary>
        public void Cancel()
        {
            if (Options.IsCancelable == false)
                throw new ApplicationException("Cannot cancel noncancelable operation.");

            State = JobState.CancelRequested;

            if (AsyncState is ITaskResult)
            {
                var state = (ITaskResult)AsyncState;
                state.Cancel();
            }
            else
            {
                throw new ApplicationException("This job does not support the cancel operation.");
            }
        }

        /// <summary>
        /// Method that encapsulates the job logic.
        /// </summary>
        private void PerformExecute(object state)
        {
            if (m_ExecutionDelegate != null)
                m_ExecutionDelegate.Invoke(state);
        }

        /// <summary>
        /// Executes the operation in the background thread giving it a name. This method is executed
        /// </summary>
        private void PerformAsyncExecute(object state)
        {
            // Set worker thread with apropriate culture info.
            Thread.CurrentThread.CurrentCulture = m_CultureInfo;
            Thread.CurrentThread.CurrentUICulture = m_CultureInfo;

            // Execute the operation
            PerformExecute(state);
        }

        /// <summary>
        /// Executes job synchronously.
        /// </summary>
        public void Run()
        {
            if (State != JobState.Ready)
                throw new ApplicationException("Cannot run already finished job.");

            try
            {
                // Save the start time
                StartTime = DateTime.Now;

                // Set the job state
                State = JobState.Running;

                // Perform work
                PerformExecute(null);

                // Save the end time
                EndTime = DateTime.Now;

                // Raise the event
                OnCompleted(EventArgs.Empty);
            }
            catch (Exception e)
            {
                OnError(new JobErrorEventArgs(e));
            }
        }

        /// <summary>
        /// Executes job asynchronously.
        /// </summary>
        public void RunAsync()
        {
            RunAsync(null);
        }

        /// <summary>
        /// Executes job asynchronously.
        /// </summary>
        /// <param name="state">Parameter to be passed to the delegate.</param>
        public void RunAsync(object state)
        {
            if (State != JobState.Ready)
                throw new ApplicationException("Cannot run already finished job.");

            // Add self to the collection of asynchronous operations
            lock (AllJobs)
            {
                AllJobs.Add(this);
            }

            // Save the start time
            StartTime = DateTime.Now;

            m_CultureInfo = Thread.CurrentThread.CurrentCulture;

            // Set the job state
            State = JobState.Running;
            AsyncParameter = state;

            // Execute job
            AsyncState = JobConfigurator.Current.Executor.AsyncExecute(this, state);
        }

        private void AddLogging(IOperation operation)
        {
            ILoggerService loggerService = null;
            IExceptionManager exceptionManager = null;
            
            if (ServiceLocator.Current != null)
            {
                loggerService = ServiceLocator.Current.GetInstance<ILoggerService>();
                exceptionManager = ServiceLocator.Current.GetInstance<IExceptionManager>();
            }

            Error += (s, e) =>
            {
                bool rethrow = false;
                if (exceptionManager != null && loggerService != null)
                {
                    rethrow = exceptionManager.HandleException(e.Exception, "UI Policy");
                    
                    // Log that operation has completed unsuccessfuly
                    loggerService.Log(string.Format(Resources.OperationError, operation.Text, TimeElapsed.TotalMilliseconds),
                        "WpfRcp.UI", TraceEventType.Warning, Resources.Operation);
                }

                // Rethrow if necessary
                if (rethrow)
                    throw e.Exception;
            };

            Completed += (s, e) =>
            {
                if (loggerService != null)
                    loggerService.Log(string.Format(Resources.OperationFinished, operation.Text, TimeElapsed.TotalMilliseconds),
                        "WpfRcp.UI", TraceEventType.Information, Resources.Operation);
            };

            Cancelled += (s, e) =>
            {
                if (loggerService != null)
                    loggerService.Log(string.Format(Resources.OperationCancelled, operation.Text, TimeElapsed.TotalMilliseconds),
                        "WpfRcp.UI", TraceEventType.Information, Resources.Operation);
            };
        }

        #region Event Launchers

        private void FireEvent<T>(Delegate d, T args)
        {
            if (d != null)
            {
                Delegate[] delegates = d.GetInvocationList();

                if (Options.ThreadOption == JobThreadOptions.JobThread)
                {
                    foreach (Delegate del in delegates)
                    {
                        LocalJobEventDispatcher.Default.Perform(del, this, args);
                    }
                }
                else if (Options.ThreadOption == JobThreadOptions.UIThread)
                {
                    foreach (Delegate del in delegates)
                    {
                        WpfJobEventDispatcher.Default.Perform(del, this, args);
                    }
                }
            }
        }

        /// <summary>
        /// Helper method that is executed when the asynchronous call completes.
        /// </summary>
        /// <param name="e">Event arguments.</param>
        private void OnCompleted(EventArgs e)
        {
            State = JobState.Completed;

            // Save the end time
            EndTime = DateTime.Now;

            // Fire Completed event
            FireEvent(Completed, e);
        }

        private void OnError(JobErrorEventArgs e)
        {
            State = JobState.Error;

            // Save the end time
            EndTime = DateTime.Now;

            // Fire Error event
            FireEvent(Error, e);
        }

        private void OnCanceled(EventArgs e)
        {
            State = JobState.Canceled;

            // Save the end time
            EndTime = DateTime.Now;

            // Fire canceled event
            FireEvent(Cancelled, e);
        }

        private void OnPropertyChanged(string propertyName)
        {
            FireEvent(m_PropertyChanged, new PropertyChangedEventArgs(propertyName));
        }

        #endregion

        #region Asynchronous Stuff

        /// <summary>
        /// Represents an interface for job asynchronous execution.
        /// </summary>
        public interface IAsyncExecutor
        {
            object AsyncExecute(Job job, object state);
        }

        /// <summary>
        /// Represents a thread pool based async executor.
        /// </summary>
        public class ThreadPoolBasedAsyncExecutor : IAsyncExecutor
        {
            private readonly ManagedThreadPool m_PoolOfThreads;

            public ThreadPoolBasedAsyncExecutor(int poolSize)
            {
                m_PoolOfThreads = new ManagedThreadPool(poolSize);
            }

            #region IAsyncExecutor Members

            public object AsyncExecute(Job job, object state)
            {
                var result = m_PoolOfThreads.QueueUserWorkItem(job.PerformAsyncExecute, state);
                result.StateChanged += (s, e) =>
                {
                    if (result.Status == TaskStatus.Completed)
                    {
                        job.OnCompleted(EventArgs.Empty);
                    }
                    else if (result.Status == TaskStatus.Cancelled)
                    {
                        job.OnCanceled(EventArgs.Empty);
                    }
                    else if (result.Status == TaskStatus.Error)
                    {
                        job.OnError(new JobErrorEventArgs(result.Exception));
                    }
                };

                return result;
            }

            #endregion
        }

        /// <summary>
        /// Represents a delegate based async executor.
        /// </summary>
        public class DelegateBasedAsyncExecutor : IAsyncExecutor
        {
            private Job m_Job;

            private void OnAsyncExecuteComplete(IAsyncResult result)
            {
                try
                {
                    var asyncResult = (AsyncResult)result;
                    var del = (JobStart)asyncResult.AsyncDelegate;

                    del.EndInvoke(result);

                    m_Job.OnCompleted(EventArgs.Empty);
                }
                catch (Exception ex)
                {
                    m_Job.OnError(new JobErrorEventArgs(ex));
                }
            }

            #region IAsyncExecutor Members

            public object AsyncExecute(Job job, object state)
            {
                m_Job = job;
                JobStart del = m_Job.PerformAsyncExecute;
                return del.BeginInvoke(state, OnAsyncExecuteComplete, null);
            }

            #endregion
        }

        #endregion

        #region IDisposable Implementation

        public void Dispose()
        {
            m_PropertyChanged = null;
            Completed = null;
            Cancelled = null;
            Error = null;
        }

        #endregion
    }
}